/*
 * Copyright (c) 2011 Imre Fazekas.
 *  All rights reserved.
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *
 *  Redistributions of source code must retain the above copyright notice, this
 *  list of conditions and the following disclaimer.
 *
 *  Redistributions in binary form must reproduce the above copyright notice,
 *  this list of conditions and the following disclaimer in the documentation
 *  and/or other materials provided with the distribution.
 *  Neither the name of the Brillien nor the names of its
 *  terms and concepts may be used to endorse or promote products derived from this
 *  software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 *  AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 *  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 *  ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 *  LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 *  CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 *  SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 *  INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *  CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 *  ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 *  POSSIBILITY OF SUCH DAMAGE.
 */

package com.vii.brillien.services.sso.shiro;

import com.vii.brillien.core.component.db.CouchPresence;
import com.vii.brillien.core.management.BrillienServices;
import com.vii.brillien.ignition.BrillienContext;
import com.vii.brillien.kernel.BrillienException;
import com.vii.streamline.services.error.StreamLineException;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.jcouchdb.db.Options;

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;

/**
 * Specialized AuthorizingRealm for couchdb databases to make Brillien-like DataSources available
 */
public class CouchRealm<U extends ShiroUser> extends AuthorizingRealm {

    public static CouchRealm                couchRealmInstance;

    protected String                        dataPresence;

    protected boolean                       permissionsLookupEnabled = true;

    protected volatile CouchPresence        couchPresenceInstance;
    
    protected String                        userClassName;
    protected Class<U>                      userClass;
    protected String                        userByUsernameQueryName;


    {
        userClassName = ShiroUser.class.getName();
        userClass = (Class<U>)ShiroUser.class;

        couchRealmInstance = this;

        userByUsernameQueryName = "/collect_user_by_username";
    }

    public String getDataPresence() {
        return dataPresence;
    }

    public void setDataPresence(String dataPresence) {
        this.dataPresence = dataPresence;
    }

    public Class<U> getUserClass() {
        return userClass;
    }

    public void setUserClass(Class<U> userClass) {
        BrillienContext.basicLog( Level.INFO, "Registered as specialized ShiroUser type!", userClass );
        this.userClass = userClass;
    }

    public String getUserByUsernameQueryName() {
        return userByUsernameQueryName;
    }

    public void setUserByUsernameQueryName(String userByUsernameQueryName) {
        this.userByUsernameQueryName = userByUsernameQueryName;
    }

    public String getUserClassName() {
        return userClassName;
    }

    public void setUserClassName(String userClassName) {
        this.userClassName = userClassName;
    }

    public boolean isPermissionsLookupEnabled() {
        return permissionsLookupEnabled;
    }

    public void checkDB() throws BrillienException, StreamLineException {
        if( couchPresenceInstance == null ){
            couchPresenceInstance = BrillienServices.getPresenceByName(BrillienServices.liaison, dataPresence);
        }
        couchPresenceInstance.checkDB();
    }

    /**
     * Enables lookup of permissions during authorization.  The default is "false" - meaning that only roles
     * are associated with a user.  Set this to true in order to lookup roles <b>and</b> permissions.
     *
     * @param permissionsLookupEnabled true if permissions should be looked up during authorization, or false if only
     *                                 roles should be looked up.
     */
    public void setPermissionsLookupEnabled(boolean permissionsLookupEnabled) {
        this.permissionsLookupEnabled = permissionsLookupEnabled;
    }

    protected Set<String> getRoleNamesForUser(String username) throws BrillienException, StreamLineException {
        checkDB();

        try{
            List<U> users = couchPresenceInstance.<U>getData(userClass, userByUsernameQueryName, new Options().key(username).limit(1));
            return new LinkedHashSet<String>( users.get(0).getRoles() );
        } catch(Exception e){
            throw new BrillienException(e.getMessage() );
        }
    }
    protected Set<String> getPermissions(String username, Collection<String> roleNames) throws BrillienException, StreamLineException {
        checkDB();

        try{
            List<U> users = couchPresenceInstance.<U>getData(userClass, userByUsernameQueryName, new Options().key(username).limit(1));
            return new LinkedHashSet<String>( users.get(0).getPermissions() );
        } catch(Exception e){
            throw new BrillienException(e.getMessage() );
        }
    }
    protected String getPasswordForUser(String username) throws BrillienException, StreamLineException {
        checkDB();

        try{
            List<U> users = couchPresenceInstance.<U>getData(userClass, userByUsernameQueryName, new Options().key(username).limit(1));
            return users.get(0).getPassword();
        } catch(Exception e){
            throw new BrillienException(e.getMessage() );
        }
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //null usernames are invalid
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }

        String username = (String) getAvailablePrincipal(principals);

        Set<String> roleNames = null;
        Set<String> permissions = null;
        try {
            // Retrieve roles and permissions from database
            roleNames = getRoleNamesForUser(username);
            if (permissionsLookupEnabled) {
                permissions = getPermissions(username, roleNames);
            }
        } catch (Throwable t) {
            final String message = "There was a SQL error while authorizing user [" + username + "]";
            BrillienContext.exceptionLog(RelationalRealm.class.getName(), "doGetAuthorizationInfo:username", t);

            // Rethrow any SQL errors as an authorization exception
            throw new AuthorizationException(message, t);
        }

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames);
        info.setStringPermissions(permissions);
        return info;
    }

    protected AuthenticationInfo buildAuthenticationInfo(String username, char[] password) {
        return new SimpleAuthenticationInfo(username, password, getName());
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) authenticationToken;
        String username = upToken.getUsername();

        // Null username is invalid
        if (username == null) {
            throw new AccountException("Null usernames are not allowed by this realm.");
        }

        AuthenticationInfo info = null;
        try {
            String password = getPasswordForUser(username);

            if (password == null) {
                throw new UnknownAccountException("No account found for user [" + username + "]");
            }

            info = buildAuthenticationInfo(username, password.toCharArray());

        } catch (Throwable t) {
            final String message = "There was a SQL error while authenticating user [" + username + "]";
            BrillienContext.exceptionLog(RelationalRealm.class.getName(), "doGetAuthenticationInfo:username", t);
            // Rethrow any SQL errors as an authentication exception
            throw new AuthenticationException(message, t);
        }

        return info;
    }

}
